<?php
/**
 * Created by paperphp
 * User: 22071
 * Date: 2019/6/25
 * Email: <zhendongdong@foxmail.com>
 */

namespace paper;


use Closure;
use paper\exception\ClassNotFoundException;
use paper\exception\Handler;
use paper\exception\MethodNotFoundException;
use paper\http\Request;
use paper\http\Response;
use paper\log\Log;
use paper\routing\Router;
use paper\session\Session;
use ReflectionException;
use ReflectionMethod;

/**
 * @property App app
 * @property Config $config
 * @property Session $session
 * @property Cookie $cookie
 * @property Request $request
 * @property Log $log
 * @property Response $response
 * @property Url $url
 * @property View $view
 * @property Router $router
 * @property Middleware $middleware
 * @property Kernel $kernel
 * @property Handler $exceptionHandler
 */
class Container
{


    protected $instances = [];
    public static $instance;

    /**
     * 容器绑定标识
     * @var array
     */
    protected $bind = [

    ];

    /**
     * 服务别名，预先保留一下系统组件的别名
     * @var array
     */
    protected $alias = [];


    /**
     * @return static
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    /**
     * @param Container|null $container
     * @return Container
     */
    public static function setInstance(Container $container = null)
    {
        return static::$instance = $container;
    }


    /**
     * 设置对象实例
     * @param $name
     * @param $concrete
     */
    protected function instance($name, $concrete)
    {
        $name                   = $this->getAlias($name);
        $this->instances[$name] = $concrete;
    }

    /**
     * 设置别名
     * @param $alias
     * @param null $abstract
     */
    public function alias($alias, $abstract = null)
    {
        if (is_array($alias)) {
            $this->alias = array_merge($this->alias, $alias);
        } else {
            $this->alias[$alias] = $abstract;
        }
    }

    /**
     * 根据别名或者全名
     * @param $name
     * @return mixed
     */
    protected function getAlias($name)
    {
        // var_dump($name);
        return $this->alias[$name] ?? $name;
    }

    /**
     * 获取容器中的对象实例
     * @access public
     * @param string $abstract 类名或者标识
     * @return object
     * @throws Exception
     */
    public function get($abstract)
    {
        return $this->make($abstract);
    }

    /**
     * 绑定单例服务
     * @param $name
     * @param null $concrete
     */
    public function singleton($name, $concrete = null)
    {
        if (is_null($concrete)) {
            $concrete = $name;
        }
        $this->bind[$name] = [$concrete, false];
    }

    /**
     * 绑定服务
     * @param $name
     * @param null $concrete
     */
    public function bind($name, $concrete = null)
    {
        if (is_null($concrete)) {
            $concrete = $name;
        }
        $this->bind[$name] = [$concrete, true];
    }


    /**
     * @param $abstract
     * @param array $vars
     * @return mixed
     */
    private function getConcrete($abstract, $vars = [])
    {
        if ($abstract instanceof Closure) {
            return $abstract($this);
        } elseif (is_string($abstract)) {
            return new $abstract($this);
        }
        return $abstract;
    }

    /**
     * @param $abstract
     * @param array $vars
     * @return mixed
     */
    public function make($abstract, $vars = [])
    {
        try {
            $abstract = $this->getAlias($abstract);
            if (isset($this->instances[$abstract])) {
                return $this->instances[$abstract];
            }
            if (!isset($this->bind[$abstract])) {
                return new $abstract($this);
            }

            [$concrete, $newInstance] = $this->bind[$abstract];
            $concrete = $this->getConcrete($concrete);
            if ($newInstance === false) {
                $this->instances[$abstract] = $concrete;
            }
            return $concrete;
        } catch (Exception $exception) {
            throw new ClassNotFoundException($exception->getMessage(), $abstract);
        }
    }


    /**
     * 执行函数或者方法并且实现依赖注入
     * @param string $abstract
     * @param string $method
     * @return Response
     * @throws ReflectionException
     */
    public function invokeMethod($abstract, $method)
    {
        $app = is_object($abstract) ?: $this->app->make($abstract);
        try {
            //if (is_callable([$app, $method], false)) {
            $params = $this->bindParams(new ReflectionMethod($abstract, $method));
            return call_user_func_array([$app, $method], $params);
            // }
        } catch (Exception $exception) {
            throw new MethodNotFoundException($method . "方法不可访问");
        }
    }

    /**
     * 执行函数或者方法并且实现依赖注入
     * @param $func
     * @return Response
     */
    public function invokeFunc($func)
    {
        return call_user_func_array($func, $this->router->getParameters());
    }

    /**
     * 绑定参数
     * @param ReflectionMethod $reflectionMethod
     * @return array
     * @throws ReflectionException
     * @throws Exception
     */
    private function bindParams(ReflectionMethod $reflectionMethod)
    {
        $parameters = [];
        foreach ($reflectionMethod->getParameters() as $parameter) {
            //var_dump($parameter);
            $parameterClass = $parameter->getClass();
            if ($parameterClass) {
                $parameters[$parameter->name] = $this->make($parameterClass->name);
            } else {
                //parameter
                if ($params = array_shift($this->app->router->getParameters())) {
                    $parameters[] = $params;
                } else {
                    $parameters[] = $parameter->getDefaultValue() ?: null;
                }
            }
        }
        return $parameters;
    }

    public function __set($abstract, $value)
    {
        if (isset($this->bind[$abstract])) {
            $abstract = $this->bind[$abstract];
        }
        $this->instances[$abstract] = $value;
    }

    /**
     * @param $name
     * @return mixed
     * @throws Exception
     */
    public function __get($name)
    {
        return $this->make($name);
    }

}